package cn.blnp.net.flowable.task.service.property;

import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.flowable.variable.api.history.HistoricVariableInstance;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
 * <h3>服务任务的属性注入</h3>
 *
 * @author <a href="mailto:blnp.yibin@qq.com">lyb</a>
 * @version 1.0
 * @since 2025/5/13 9:15
 */
@Slf4j
@SpringBootTest
public class ServicePropTest {

    @Resource
    private RepositoryService repositoryService;
    @Resource
    private RuntimeService runtimeService;
    @Resource
    private TaskService taskService;
    @Resource
    private HistoryService historyService;
    
    /**
     *  <p>
     *      通过flowable:class指定实现JavaDelegate接口的类，或通过flowable:delegateExpression指定委托表达式，都可以实现为属性注入数据的目的。Flowable支持如下类型的注入。
     *      <ul>
     *          <li>固定字符串。对于某些常量，可以直接在流程定义中配置字符串注入。</li>
     *          <li>表达式。对于某些变量或者对象，可以将其配置为UEL表达式注入。</li>
     *      </ul>
     *  </p>
     *  <h2>固定字符串注入</h2>
     *  <h3>案例演示：</h3>
     *  <hr/>
     *  <p>
     *      将常量值注入服务任务指定的Java类中声明的字段的示例，服务类详见：{@link CalculationFieldInjected}。这与服务任务定义中声明要注入的属性名一致。这些属性都提供了setter()方法
     *  （这里使用Lombok的@Setter注解实现），并且属性类型均为org.flowable.common.engine.api.delegate.Expression，通过调用它们的getValue(execution)方法即可得到注入的属性值。execute()方法
     *  的核心逻辑是，首先通过调用属性的getValue(execution)方法分别获取注入的属性值，然后进行计算操作并输出日志，最后将计算结果放入流程变量中。需要注意的是，无论在服务任务定义中声明的属性值是何种类型，
     *  注入目标Java类中的属性类型都应该为org.flowable.common.engine.api. delegate.Expression，经表达式解析后可以将其转换为对应的类型。<br/>
     *      <b>流程定义文件：</b>{@link flow/serviceTaskFieldInjected.bpmn20.xml}<br/>
     *      <b>流程预览图：</b><br/>
     *      <img src="https://z.wiki/u/eVrJ2Y" />
     *  </p>
     **/
    @Test
    public void testInjectFixed() {
        Deployment deploy = repositoryService.createDeployment()
                .name("服务任务属性注入案例")
                .addClasspathResource("flow/serviceTaskFieldInjected.bpmn20.xml")
                .deploy();
        log.info("部署流程定义：{}", deploy.getId());
        ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery()
                .deploymentId(deploy.getId())
                .singleResult();
        log.info("流程定义编号：{}", procDef.getId());
        //启动流程实例
        ProcessInstance procInst = runtimeService.startProcessInstanceById(procDef.getId());
        //查询历史流程变量
        HistoricVariableInstance historicVariable = historyService
                .createHistoricVariableInstanceQuery()
                .processInstanceId(procInst.getId())
                .variableName("totalAmount")
                .singleResult();
        log.info("totalAmount的值为：{}", historicVariable.getValue());

        /**
         *  演示结果：
         * 2025-05-13 09:38:09.466  INFO 112764 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : 部署流程定义：eca831d9-2f9a-11f0-8cb0-50ebf6ec8f6d
         * 2025-05-13 09:38:09.469  INFO 112764 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : 流程定义编号：serviceTaskFieldInjected:1:ece4507c-2f9a-11f0-8cb0-50ebf6ec8f6d
         * 2025-05-13 09:38:09.482  INFO 112764 --- [           main] c.b.n.f.t.s.p.CalculationFieldInjected   : 单价：100.0，数量：10，总价：1000.0
         * 2025-05-13 09:38:09.482  INFO 112764 --- [           main] c.b.n.f.t.s.p.CalculationFieldInjected   : 描述信息：这是一段比较长的描述信息
         * 2025-05-13 09:38:09.527  INFO 112764 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : totalAmount的值为：1000.0
         **/
    }

    /**
     *  <h2>UEL表达式注入</h2>
     *  <hr/>
     *  <p>
     *      可以使用UEL表达式注入在运行时动态解析的值，这些表达式可以使用流程变量或Spring容器的Bean。前面提到，当服务任务使用flowable:class属性指定调用的Java类时，该Java类的
     *  一个实例将在服务任务所在的所有流程实例之间共享。为了动态注入属性的值，可以在org.flowable.common.engine.api.delegate.Expression中使用值和方法表达式，它会使用传递给execute()方法
     *  的DelegateExecution参数进行解析。
     *  </p>
     *  <h3>案例演示：</h3>
     *  <p>
     *      该流程为采购总价计算流程：“提交采购单”用户任务提交后，将由“计算采购总价”服务任务计算采购费用。<br/>
     *      <b>流程定义文件：</b>{@link flow/serviceTaskUelFieldInjected.bpmn20.xml}<br/>
     *      <b>简化配置，流程定义文件：</b>{@link flow/serviceTaskUelFieldInjected2.bpmn20.xml}<br/>
     *      <b>流程预览图：</b><br/>
     *      <img src="https://cdn4.winhlb.com/2025/05/13/6822a5743108a.png" />
     *  </p>
     **/
    @Test
    public void testInjectUel() {
        Deployment deploy = repositoryService.createDeployment()
                .name("服务任务属性UEL注入案例")
                .addClasspathResource("flow/serviceTaskUelFieldInjected.bpmn20.xml")
                .deploy();
        log.info("部署流程定义：{}", deploy.getId());
        ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery()
                .deploymentId(deploy.getId())
                .singleResult();
        log.info("流程定义编号：{}", procDef.getId());
        //启动流程实例
        ProcessInstance procInst = runtimeService.startProcessInstanceById(procDef.getId());

        //查询第一个任务
        Task task = taskService.createTaskQuery()
                .processInstanceId(procInst.getId())
                .singleResult();
        //初始化流程变量
        Map<String, Object> varMap = MapUtil.builder(new HashMap<String, Object>())
                .put("unitPrice", 100.0)
                .put("quantity", 4)
                .put( "description", "此次采购了4个单价100元的办公耗材")
                .put("calculationBean", new CalculationBean())
                .build();
        //办理第一个任务
        taskService.complete(task.getId(), varMap);
        //查询历史流程变量
        HistoricVariableInstance historicVariableInstance = historyService
                .createHistoricVariableInstanceQuery()
                .processInstanceId(procInst.getId())
                .variableName("totalAmount")
                .singleResult();
        log.info("totalAmount的值为：{}", historicVariableInstance.getValue());

        /**
         *  演示结果：
         *  2025-05-13 09:55:55.259  INFO 96792 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : 部署流程定义：6810131d-2f9d-11f0-adb5-50ebf6ec8f6d
         * 2025-05-13 09:55:55.262  INFO 96792 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : 流程定义编号：6829dcaf-2f9d-11f0-adb5-50ebf6ec8f6d
         * 2025-05-13 09:55:55.398  INFO 96792 --- [           main] .b.n.f.t.s.p.CalculationUelFieldInjected : 单价：100.0，数量：4，总价：400.0
         * 2025-05-13 09:55:55.398  INFO 96792 --- [           main] .b.n.f.t.s.p.CalculationUelFieldInjected : 描述信息：此次采购了4个单价100元的办公耗材
         * 2025-05-13 09:55:55.454  INFO 96792 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : totalAmount的值为：400.0
         **/
    }

    /**
     *  <p>
     *      <b style="color:red;">（线程安全）</b>一般来说，服务任务使用的以上两种属性注入方式是线程安全的。但是，在某些情况下，则不能保证这种线程的安全性。这取决于Flowable设置的运行环境。
     *  使用flowable:class属性时，使用属性注入总是线程安全的。引用了某个Java类的每一个服务任务都会实例化新的实例，并且在创建实例时注入一次字段。在不同的服务任务或流程定义中多次重复使用同一个Java类是没有问题的。
     *  </p>
     *  <p>
     *      <b style="color:red;">（线程安全）</b>使用flowable:expression属性时，不能使用属性注入，只能通过方法调用传递参数，这些参数总是线程安全的。
     *  </p>
     *  <p>
     *      &emsp;&emsp;使用flowable:delegateExpression属性时，使用属性注入的线程安全性将取决于表达式的解析方式。如果该委托表达式在多个服务任务或流程定义中重复使用，并且表达式总是返回相同的实例，则使用属性注入
     *   不是线程安全的。<br/>
     *      1、<b style="color:red;">（安全）</b>假设表达式是${delegateBean.doSomething(someVariable)}，其中delegateBean是工作流引擎引用的Java Bean（如与Spring集成后由Spring托管的Bean），
     *   并在每次解析表达式时都会创建一个新实例（如Spring的prototype模式）。在这种情况下，使用属性注入是线程安全的，因为每次解析表达式时，这些属性都会注入这个新实例。<br/>
     *      2、<b style="color:red;">（不安全）</b>如果表达式为${someJavaDelegateBean}，它解析为JavaDelegate类的实现，并且在创建单例的环境（如Spring的singleton模式）中运行。当在不同的
     *   服务任务或流程定义中使用这个表达式时，表达式总会解析为相同的实例。在这种情况下，使用属性注入就不是线程安全的了。
     *  </p>
     *  <h2>案例举证</h2>
     *  <hr/>
     *  <p>
     *      下述示例配置中定义了两个服务任务：serviceTask1和serviceTask2。它们通过flowable:delegateExpression指定了同一个表达式，但注入someField属性时使用expression配置了不同的表达式。如果flowable:delegateExpression指定
     *  的表达式解析为相同的实例，那么在并发场景下，注入someField属性时可能会产生竞争。为了避免出现这种问题，可以使用flowable:expression方式替代flowable:delegateExpression方式，通过方法参数传递所需的数据，也可以在每次委托表
     *  达式解析时返回委托类的新实例，如在与Spring集成时将Bean的scope参数设置为prototype（在类上添加@Scope(SCOPE_PROTOTYPE)注解）。
     *  <pre>
     *      {@code
     *          <serviceTask id="serviceTask1" flowable:delegateExpression="${someJavaDelegateBean}">
     *              <extensionElements>
     *                  <flowable:field name="someField" expression="${input * 2}"/>
     *              </extensionElements>
     *          </serviceTask>
     *          <serviceTask id="serviceTask2" flowable:delegateExpression="${someJavaDelegateBean}">
     *              <extensionElements>
     *                  <flowable:field name="someField" expression="${input * 2000}"/>
     *              </extensionElements>
     *          </serviceTask>
     *      }
     *  </pre>
     *  </p>
     *  <h3 style="color:#FFA500;">特别说明：</h3>
     *  <p>
     *      当使用flowable:delegateExpression属性时，可以通过配置工作流引擎配置delegateExpressionFieldInjectionMode属性设置在委托表达式上使用属性注入模式，代码如下：
     *  <pre>
     *      {@code
     *          <bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
     *              <!-- 设置delegateExpressionFieldInjectionMode参数 -->
     *              <property name="delegateExpressionFieldInjectionMode" value="MIXED"/>
     *          </bean>
     *      }
     *  </pre>
     *  代码配置delegateExpressionFieldInjectionMode属性为MIXED。delegateExpressionFieldInjectionMode属性可以取{@link org.flowable.engine.impl.cfg.DelegateExpressionFieldInjectionMode}枚举中的以下值。
     *  <ul>
     *      <li>
     *          <b>DISABLED：</b>禁用模式，表示当使用委托表达式时完全禁用属性注入。这是<u>最安全的一种方式，保证线程安全</u>。
     *      </li>
     *      <li>
     *          <b>COMPATIBILITY：</b>兼容模式，表示可以在委托表达式中使用属性注入。如果在配置的委托类中没有定义要注入的字段，会抛出 {@link org.flowable.common.engine.api.FlowableIllegalArgumentException}
     *          异常，异常信息为Field definition uses non-existent field *** of class ***。这是线程最不安全的模式，但可以保证历史版本的兼容性，也可以在委托表达式只在一个任务中使用时安全使用（不会产生并发竞争）。
     *      </li>
     *      <li>
     *          <b>MIXED：</b>混合模式，表示可以在使用委托表达式时注入，但当配置的委托类中没有定义要注入的字段时不会抛出异常。这是Flowable 6中的默认模式。这样可以在部分委托（如非单例的实例）中使用注入，而在部分委托中不使用注入。
     *      </li>
     *  </ul>
     *      <b>流程定义文件：</b>{@link flow/serviceTaskDelegateExpressionFieldInjected.bpmn20.xml}<br/>
     *      <b>流程预览图：</b><br/>
     *      <img src="https://cdn4.winhlb.com/2025/05/13/6822b0ac617c9.png" />
     *  </p>
     **/
    @Test
    public void testNote() {
        Deployment deploy = repositoryService.createDeployment()
                .name("服务任务属性线程安全测试案例")
                .addClasspathResource("flow/serviceTaskDelegateExpressionFieldInjected.bpmn20.xml")
                .deploy();
        log.info("部署流程定义：{}", deploy.getId());
        ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery()
                .deploymentId(deploy.getId())
                .singleResult();
        log.info("流程定义编号：{}", procDef.getId());
        //启动流程实例
        ProcessInstance procInst = runtimeService.startProcessInstanceById(procDef.getId());

        //查询第一个任务
        Task task = taskService.createTaskQuery()
                .processInstanceId(procInst.getId()).singleResult();
        //初始化流程变量
        Map<String, Object> varMap = MapUtil.builder(new HashMap<String, Object>())
                .put("unitPrice", 100.0)
                .put("quantity", 4)
                .put("description", "此次采购了4个单价100元的办公耗材")
                .build();
        //初始化瞬时变量
        Map<String, Object> traVarMap = MapUtil.builder(new HashMap<String, Object>())
                .put("calculationDelegateExpressionFieldInjected",
                        new CalculationDelegateExpressionFieldInjected())
                .build();
        //办理第一个任务
        taskService.complete(task.getId(), varMap, traVarMap);
        //查询历史流程变量
        HistoricVariableInstance historicVariableInstance = historyService
                .createHistoricVariableInstanceQuery()
                .processInstanceId(procInst.getId())
                .variableName("totalAmount")
                .singleResult();
        log.info("totalAmount的值为：{}", historicVariableInstance.getValue());

        /**
         *  演示结果：
         * 2025-05-13 10:40:27.336  INFO 95132 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : 部署流程定义：a0a555bf-2fa3-11f0-806d-50ebf6ec8f6d
         * 2025-05-13 10:40:27.338  INFO 95132 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : 流程定义编号：a0d78952-2fa3-11f0-806d-50ebf6ec8f6d
         * 2025-05-13 10:40:27.419  INFO 95132 --- [           main] lculationDelegateExpressionFieldInjected : 单价：100.0，数量：4，总价：400.0
         * 2025-05-13 10:40:27.419  INFO 95132 --- [           main] lculationDelegateExpressionFieldInjected : 描述信息：此次采购了4个单价100元的办公耗材
         * 2025-05-13 10:40:27.474  INFO 95132 --- [           main] c.b.n.f.t.s.property.ServicePropTest     : totalAmount的值为：400.0
         **/
    }
}
